Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Acknowledge reception of data in TrinoResult #220

Merged
merged 4 commits into from
Sep 22, 2022

Conversation

mdesmet
Copy link
Contributor

@mdesmet mdesmet commented Aug 12, 2022

Description

Fixes #232, #95

Ensures the received data is properly acknowledged by calling the next_uri. This will avoid seeing failed queries in the query log when executing scalar queries as in the following example.

cur.execute("SELECT VERSION()")
cur.fetchone()
cur.cancel()

Release notes

( ) This is not user-visible and no release notes are required.
( ) Release notes are required, please propose a release note for me.
(x) Release notes are required, with the following suggested text:

## Breaking Changes

* Make the `execute` method of the cursor block until at-least one row is received.
  This means users no longer need to call `fetchone` or `fetchall` to make sure query
  actually starts executing on the Trino server. Note that results still need to be consumed
  by calling `fetchone` or `fetchall` to make sure query isn't considered idle and terminated
  on the server. ([#232](https://trinodb/trino-python-client/issues/232))
* Properly propagate query failures to the client when using `fetchone`.
  ([#95](https://trinodb/trino-python-client/issues/95))
* Fix queries returning a single row from sometimes appearing as failed on the server.
  ([#220](https://trinodb/trino-python-client/issues/220))

trino/client.py Outdated Show resolved Hide resolved
@mdesmet
Copy link
Contributor Author

mdesmet commented Aug 14, 2022

The CI issues can be simulated by following code. Seems that although the result has returned the Trino API still provides one or more next_uri's to fetch in a minority of cases.

def test_query_cancellation_not_triggered(trino_connection):
    count_not_finished = 0
    for _ in range(0, 1000):
        cur = trino_connection.cursor()
        cur.execute("SELECT VERSION()")
        cur.fetchone()
        if not cur._query.finished:
            count_not_finished += 1

    print(str(count_not_finished) + " unfinished queries")

Some questions:

  • can we handle this on the serverside to not let this type of queries fail if all results have been consumed?
  • Why do we mark a cancelled query as failed if it's a valid use case to only retrieve a number of results and bail out?

@mdesmet mdesmet requested review from hashhar and ebyhr August 15, 2022 14:15
@mdesmet mdesmet force-pushed the bug/consume-results branch 2 times, most recently from e35d986 to 5d59610 Compare August 16, 2022 11:19
@hashhar
Copy link
Member

hashhar commented Aug 16, 2022

Why do we mark a cancelled query as failed if it's a valid use case to only retrieve a number of results and bail out?

It's not valid. It's a hack that BI tools and clients use instead of limiting their query to pull only what is needed.
e.g. As a cluster admin it's very useful to see clients who run a query that returns billions of rows but just take 100 rows and leave the query hanging (instead of either cancelling or consuming results) which means that the output buffer on the server (and other processes) keeps occupying memory until the query times out.

can we handle this on the serverside to not let this type of queries fail if all results have been consumed?

You know that version() would return one row but the server does not since results are streamed back to coordinator from workers and coordinator can't know that there isn't more data coming until it asks workers about it.

Change your experiment to a query which returns arbitrary number of rows and then you can't know anymore whether query is finished or not. Special casing the client protocol for queries which return single row doesn't seem useful.

@hashhar
Copy link
Member

hashhar commented Aug 16, 2022

The alternative is to have a client protocol which is based on persistent TCP connections instead of HTTP long polling - which brings it's own set of problems.

@hashhar
Copy link
Member

hashhar commented Aug 16, 2022

Also, specifically on why you cannot assume an empty rows being returned from API as proof that query has finished is the pipelined execution model. In queries it's possible for Trino to perform both table scans and output results at the same time. e.g. If you have a long chain of UNION ALL statements then Trino can start returning results as soon as the first UNION ALL query is done while other parts of the query are still executing .

This means that the client might observe periods of time where there is no data returned but a nextUri is still included. If the client were to assume no data == query finished then it'll drop any upcoming rows that will be produced.

@nineinchnick
Copy link
Member

@hashhar would you agree that to address the original issue we should add a fetch call after scalar() to drain the cursor? We might also want to document this somewhere. I don't think we should try to make the driver too smart to work around these protocol limitations.

@hashhar
Copy link
Member

hashhar commented Aug 16, 2022

@nineinchnick I don't agree 100%. It's still useful to make the client as smart as the JDBC driver where the implementation detail of the Trino REST protocol isn't visible to users.

But yes, since this might take more time than the quickfix I think we should go with the quickfix for now and then think about how to stop the protocol from leaking into user code.

@mdesmet
Copy link
Contributor Author

mdesmet commented Aug 16, 2022

@hashhar would you agree that to address the original issue we should add a fetch call after scalar() to drain the cursor? We might also want to document this somewhere. I don't think we should try to make the driver too smart to work around these protocol limitations.

This is exactly what sqlalchemy does when you scalar_one or scalar_one_or_none. Again we already fetch the next row in client module's fetch now, but even with fetching another record we have no guarantee that would finish the query (nextUri set to null) as @hashhar said.

https://github.com/sqlalchemy/sqlalchemy/blob/f8c4dba4e9f130c18ce00597c036bc26ae7abf90/lib/sqlalchemy/engine/result.py#L745-L748

@mdesmet mdesmet requested a review from hashhar August 18, 2022 10:56
@mdesmet mdesmet marked this pull request as draft September 13, 2022 14:18
@mdesmet mdesmet force-pushed the bug/consume-results branch 4 times, most recently from 2bec10e to fb66f9c Compare September 13, 2022 22:38
@mdesmet mdesmet marked this pull request as ready for review September 13, 2022 22:39
trino/client.py Outdated Show resolved Hide resolved
@mdesmet
Copy link
Contributor Author

mdesmet commented Sep 17, 2022

However this is also a breaking change (even though it improves experience) so I'm approving it but not merging until we release a version from current master so that people can upgrade to that version and then choose to decide whether they want to stay there for sometime before migrating to new blocking API.

Can you explain why you think this is a breaking change? IMHO the only way to break existing usage is if users didn't catch exceptions on execute(), which is a bug already as query submission can throw the same exceptions as fetch*(). The fetch*() operations continue to work as before, as proven by the integration tests.

Note that sqlalchemy doesn't call fetch*() on DML operations. Bringing this in would fix that.

And now that we are taking this direction it might be wise to decouple the Trino API handling from the db-api client and instead make available the fetched rows to the db-api cursor via a queue (list) instead of it directly fetching things from API. That gives future flexibility to introduce performance enhancements as well like the double-bufferring that the JDBC driver does for example and also make it possible to provide different cursor implementations.

IMHO this is already decoupled. The TrinoQuery exists in client module and the exposes a lazy collection (an Iterator powered by a generator). I think this is the correct abstraction to use. I don't see leakage of the API details being introduced in this PR, actually the opposite is true: I would argue that in current code execute() is only query submission while fetch*() is query execution and result set scrolling, while this PR makes execute() query submission and execution and fetch*() result set scrolling which seems semantically more correct and in line with other dbapi implementations.

The double buffering as in the java Trino client, is implemented in this PR. Note that also the Java client doesn't use threading at this moment. I think it is a good idea to investigate but can be done independent from this PR.

I don't see why cursor implementations are impacted, cursors would take the Iterable and convert it for example in a dict instead of a tuple (many dbapi implementations have a DictCursor).

@hashhar
Copy link
Member

hashhar commented Sep 20, 2022

Can you explain why you think this is a breaking change?

Because the API is now blocking.

I don't see leakage of the API details being introduced in this PR, actually the opposite is true:

I don't mean that this PR leaks the API details. I meant the opposite. Now we are one step closer to hide the API details within TrinoQuery and TrinoResult. An example of what this PR allows to do (but probably doesn't make sense) is to have a different impl of TrinoQuery which can probably use a different fictional transport mechanism to talk to Trino (instead of the REST API).

The double buffering as in the java Trino client, is implemented in this PR.

True

Note that also the Java client doesn't use threading at this moment. I think it is a good idea to investigate but can be done independent from this PR.

Exactly what I said above.

I don't see why cursor implementations are impacted, cursors would take the Iterable and convert it for example in a dict instead of a tuple (many dbapi implementations have a DictCursor).

Again exactly what I said your PR allows us to do in future.

trino/sqlalchemy/dialect.py Show resolved Hide resolved
@hashhar
Copy link
Member

hashhar commented Sep 20, 2022

Newer Trino versions will include trinodb/trino#14122 which can mean CI can be green without this change.

I think we should add one more entry to matrix with 395 as the version being tested for the meantime.

@mdesmet
Copy link
Contributor Author

mdesmet commented Sep 21, 2022

Newer Trino versions will include trinodb/trino#14122 which can mean CI can be green without this change.

I think we should add one more entry to matrix with 395 as the version being tested for the meantime.

I added the entry.

Copy link
Member

@hashhar hashhar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledge reception of data in TrinoResult fails without Execute should block until at least one row is received

So it seems we need to squash those.

LGTM otherwise.

A query only transitions to a FINISHED state when the results are fully consumed. The reception of the data is acknowledged by calling the next_uri before exposing the data through dbapi.

`dbapi.execute()` will now block until the first result is received. This will ensure the result set is exhausted for DML queries and as such remove the need to for calling `dbapi.fetchall()`.
@mdesmet
Copy link
Contributor Author

mdesmet commented Sep 22, 2022

@hashhar: Squashed those commits. Are we good to go?

@hashhar
Copy link
Member

hashhar commented Sep 22, 2022

Thanks. Good to go.

@hashhar hashhar merged commit 5bdc073 into trinodb:master Sep 22, 2022
@mdesmet mdesmet deleted the bug/consume-results branch September 22, 2022 19:39
@hashhar hashhar mentioned this pull request Nov 21, 2022
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

execute does not kick off query on Trino Server
5 participants